package fons.navigator.views.island.maps;

import java.awt.Color;
import java.awt.Shape;
import java.util.Map;
import java.util.Random;
import java.util.Set;

import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import edu.umd.cs.piccolo.activities.PActivity;
import edu.umd.cs.piccolo.event.PBasicInputEventHandler;
import edu.umd.cs.piccolo.event.PInputEvent;
import fons.navigator.util.piccolo.Lasso;
import fons.navigator.util.piccolo.PButton;
import fons.navigator.views.island.Cluster;
import fons.navigator.views.island.ColorField;
import fons.navigator.views.island.Island;
import fons.navigator.views.island.MapModel;
import fons.navigator.views.island.VisualIsland;

public class ForceDirectedMapModel extends MapModel {
	private final long COLOR_UPDATE_TIME = 250;
	private final Random random = new Random();
	private final Map<Island, Stress> stress = Maps.newHashMap();
	private ColorField colorField;
	private final Lasso lasso;
	
	private static class Stress {
		public double x, y;

		public Stress (double x, double y) {
			this.x = x;
			this.y = y;
		}
	}
	
	private class PlacementAnimation extends PActivity {
		private long lastTime = 0;
		private long colorUpdate = 0;
		
		public PlacementAnimation(long aDuration) {
			super(aDuration);
		}

		@Override
		protected void activityStep(long elapsedTime) {
			final long dt = elapsedTime - lastTime;
			colorUpdate += dt;
			if (colorUpdate > COLOR_UPDATE_TIME) {
				colorUpdate = 0;
				updateColorField();
			}
			step(1/40.0);						
			lastTime = elapsedTime;
		}
	}
	
	public ForceDirectedMapModel(VisualIsland visualIsland) {
		super(visualIsland);
		
		for (Island island: getVisualIsland().getIslands()) {
			stress.put(island, new Stress(0, 0));
			island.setOffset(random.nextDouble() / getVisualIsland().getValueScale(),
					random.nextDouble() / getVisualIsland().getValueScale());
			island.scaleAboutPoint(0.25 + island.getMass() / getVisualIsland().getValueSpaces().size(), 0, 0);
		}
		
		lasso = new Lasso(getVisualIsland().getCamera(), Color.red) {
			@Override
			public void selectionMade(Shape shape) {
				final Cluster cluster = new Cluster(getVisualIsland(), "Test-Cluster");
				for (Island island: getVisualIsland().getIslands()) {
					if (shape.contains(island.getOffset())) {
						cluster.addIsland(island);
						island.setPaint(Color.RED);
					}
				}
				getVisualIsland().newCluster(cluster);
			}
		};
		getVisualIsland().getCamera().addInputEventListener(lasso);
	}

	@Override
	public void step(double time) {
		calculateStress();
		releaseStress(time);
		updateIslandColors();
	}

	private void updateIslandColors() {
		/*final double scale = getVisualIsland().getValueScale();
		for (Island island: getVisualIsland().getIslands()) {
			island.setPaint(getColorField().at(island.getXOffset()*scale, island.getYOffset()*scale, null));
		}*/
	}

	private void releaseStress(double time) {
		for (Island island: stress.keySet()) {
			final double sx = stress.get(island).x;
			final double sy = stress.get(island).y;
			island.move(sx * time / 2, sy * time / 2);
		}
	}

	private void calculateStress() {
		resetStress();

		final Set<Island> otherIslands = Sets.newHashSet(getVisualIsland().getIslands());
		final double scale = getVisualIsland().getValueScale();

		for (Island island: getVisualIsland().getIslands()) {
			otherIslands.remove(island);

			for (Island other: otherIslands) {
				double dx = scale * (other.getXOffset() - island.getXOffset());
				if (dx == 0) dx = 0.01;

				double dy = scale * (other.getYOffset() - island.getYOffset());
				if (dy == 0) dy = 0.01;

				final double distance = Math.sqrt(dx*dx + dy*dy);
				final double weightDistance = island.weightDistance(other);
				final double strain = (distance - weightDistance) 
								/ (scale * Math.sqrt(getVisualIsland().getIslands().size()));			

				if (!getVisualIsland().getAverageIslands().contains(other)) {
					stress.get(island).x += dx * strain;
					stress.get(island).y += dy * strain;
				}
				
				if (!getVisualIsland().getAverageIslands().contains(island)) {
					stress.get(other).x += -dx * strain;
					stress.get(other).y += -dy * strain;
				}
			}
		}
	}

	private void resetStress() {
		for (Stress stress: this.stress.values()) {
			stress.x = 0;
			stress.y = 0;
		}
	}

	@Override
	public ColorField getColorField() {
		if (colorField == null) updateColorField();
		return colorField;
	}
	
	@Override
	public void updateColorField() {
		colorField = new ForceColorField(getVisualIsland());
		getVisualIsland().getMapGrid().updateColors();
	}

	@Override
	public void createButtons() {
		getButtonsPanel().addButton(new PButton("  <<-->>  "),
				new PBasicInputEventHandler() {
			@Override
			public void mouseClicked(PInputEvent event) {
				getVisualIsland().setValueScale(getVisualIsland().getValueScale() * 0.9);
				getVisualIsland().addActivity(new PlacementAnimation(1000));
			}
		});

		getButtonsPanel().addButton(new PButton("  >>--<<  "),
				new PBasicInputEventHandler() {
			@Override
			public void mouseClicked(PInputEvent event) {
				getVisualIsland().setValueScale(getVisualIsland().getValueScale() * 1.1);
				getVisualIsland().addActivity(new PlacementAnimation(1000));
			}
		});

		getButtonsPanel().addButton(new PButton(" Step "),
				new PBasicInputEventHandler() {
			@Override
			public void mouseClicked(PInputEvent event) {
				getVisualIsland().addActivity(new PlacementAnimation(3000));
			}			
		});
	}

}
